<?php
/**
 * 使用“令牌桶”算法实现限流
 * 基于Redis做计数
 */
declare(strict_types=1);

namespace Libraries;

use Libraries\Connections\RedisConn;
use Exception;

class RateLimit
{
    /**
     * 是否被限制
     * @param string $strId 标识，一般是IP
     * @param int $intNum 限制的次数
     * @param int $intSec 在多少秒内
     * @param string $strRedisName Redis连接名称，跟配置文件关联
     * @return bool
     * @throws Exception
     */
    public static function check(string $strId, int $intNum, int $intSec, string $strRedisName = ''): bool
    {
        $strId = trim($strId);
        if (!strlen($strId)) {
            throw new Exception('标识不能为空字符');
        }
        if ($intNum <= 0 || $intSec <= 0) {
            throw new Exception('限定的数量或时间，必须是大于0的整数');
        }
        $objRedis = RedisConn::get($strRedisName);
        if (!$objRedis) {
            RedisConn::put(null, $strRedisName);
            throw new Exception('获取Redis连接失败');
        }
        $strKey = 'rate_limit:' . md5($strId);
        $objRedis->watch($strKey);
        $arrData = $objRedis->hGetAll($strKey);
        $arrData || $arrData = [];
        $floN = $arrData['n'] ?? 0;
        $intNow = time();
        $blnRe = true;
        if ($arrData) {
            $intT = $arrData['t'] ?? ($intNow - 1);
            $floN += ($intNum / $intSec) * ($intNow - $intT) - 1;
            $floN = min($intNum, $floN);
            $blnRe = ($floN >= 0);
        } else {
            $floN = $intNum - 1;
        }
        if ($blnRe) {
            $arrData = ['n' => $floN, 't' => $intNow];
            $objRedis->multi();
            $objRedis->hMset($strKey, $arrData);
            $objRedis->expire($strKey, $intSec);
            $arrTemp = $objRedis->exec();
            $blnRe = (is_array($arrTemp) && count($arrTemp));
        } else {
            $objRedis->unwatch();
        }
        RedisConn::put($objRedis, $strRedisName);
        return $blnRe;
    }
}
